package com.leosam.tvbox.mv.proxy;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.security.spec.AlgorithmParameterSpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.leosam.tvbox.mv.utils.StringUtils;

import io.lindstrom.m3u8.model.KeyMethod;
import io.lindstrom.m3u8.model.MasterPlaylist;
import io.lindstrom.m3u8.model.MediaPlaylist;
import io.lindstrom.m3u8.model.MediaSegment;
import io.lindstrom.m3u8.model.SegmentKey;
import io.lindstrom.m3u8.model.Variant;
import io.lindstrom.m3u8.parser.MasterPlaylistParser;
import io.lindstrom.m3u8.parser.MediaPlaylistParser;
import io.lindstrom.m3u8.parser.ParsingMode;
import io.lindstrom.m3u8.parser.PlaylistParserException;

/**
 * M3u8媒体
 */
public class M3u8Media implements Downloadable {
    public static final String NAME_INDEX_M3U8_ORIGIN = "index.origin.m3u8";
    public static final String NAME_INDEX_M3U8_NOADS = "index.noads.m3u8";
    public static final String NAME_INDEX_M3U8 = "index.m3u8";
    private static final String SUF_TMP = ".tmp";
    private static final String SUF_TS = ".ts";
    private static final ObjectMapper OBJ_MAPPER = new ObjectMapper();
    private static final Logger logger = LoggerFactory.getLogger(M3u8Media.class);

    private static MasterPlaylistParser masterPlaylistParser = new MasterPlaylistParser(ParsingMode.LENIENT);
    private static MediaPlaylistParser mediaPlaylistParser = new MediaPlaylistParser(ParsingMode.LENIENT);

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    private final ThreadLocal<byte[]> BUFFER = new ThreadLocal<byte[]>() {
        @Override
        protected byte[] initialValue() {
            return new byte[4096];
        }
    };

    /**
     * 下载参数
     */
    private M3u8DownloadOptions options;

    /**
     * 影片名
     */
    private String mediaName;

    /**
     * 播放列表地址
     */
    private String originUrl;

    /**
     * 自定义请求头
     */
    private Map<String, String> requestHeaderMap = Collections.emptyMap();

    /**
     * 媒体资源
     * 
     * @param url       媒体播放url
     * @param mediaName 格式如：影片名/剧集名
     */
    public M3u8Media(String url, String mediaName) {
        this.originUrl = url;
        this.mediaName = mediaName;
        this.options = new M3u8DownloadOptions();
    }

    private class DownloadContext {
        private final BlockList blockList;

        public DownloadContext(BlockList blockList) {
            this.blockList = blockList;
        }
    }

    /**
     * M3u8片段
     */
    private class M3u8Segment implements Downloadable {

        /**
         * 上下文
         */
        private final DownloadContext ctx;

        /**
         * 原始片段信息
         */
        private final MediaSegment segment;

        /**
         * 片段序号
         */
        private final String fileName;

        /**
         * 下一个片段名
         */
        private final String nextFileName;

        /**
         * 分片
         * 
         * @param ctx          上下文
         * @param segment      分片信息
         * @param fileName     当前文件名
         * @param nextFileName 下一个文件名
         */
        private M3u8Segment(DownloadContext ctx, MediaSegment segment, String fileName, String nextFileName) {
            this.ctx = ctx;
            this.segment = segment;
            this.fileName = fileName;
            this.nextFileName = nextFileName;
        }

        /**
         * 解密输入流
         * 
         * @param inputStream 入流
         * @return 返回解密后输入流，如果流未加密则返回is
         */
        private InputStream decryptInputStream(InputStream inputStream) {
            if (segment.segmentKey().isPresent()) {
                SegmentKey segmentKey = segment.segmentKey().get();
                switch (segmentKey.method()) {
                    case NONE:
                        return inputStream;
                    case AES_128: {
                        try {
                            byte[] keyBytes = getKeyUrlContent(segmentKey.uri().get());
                            byte[] ivBytes;
                            String iv = segmentKey.iv().get();
                            ivBytes = iv.startsWith("0x") ? StringUtils.hexStringToByteArray(iv.substring(2))
                                    : iv.getBytes();
                            if (ivBytes.length != 16)
                                ivBytes = Arrays.copyOf(ivBytes, 16);

                            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
                            SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");

                            // 如果m3u8有IV标签，那么IvParameterSpec构造函数就把IV标签后的内容转成字节数组传进去
                            AlgorithmParameterSpec paramSpec = new IvParameterSpec(ivBytes);

                            cipher.init(Cipher.DECRYPT_MODE, keySpec, paramSpec);
                            CipherInputStream cis = new CipherInputStream(inputStream, cipher);
                            return cis;
                        } catch (MalformedURLException | NoSuchAlgorithmException | NoSuchPaddingException
                                | InvalidKeyException | InvalidAlgorithmParameterException e) {
                            throw new M3u8DecryptException("解密失败", e);
                        }
                    }
                    default:
                        // FIXME: 完善其他算法
                        return inputStream;
                }
            } else {
                return inputStream;
            }
        }

        /**
         * 从远程服务器读取解密好的输入流
         * 
         * @return 返回输入流
         * @throws IOException 任何错误
         */
        public InputStream getInputStream() throws IOException {

            // 模拟http请求获取ts片段文件
            URL url = new URL(segment.uri());

            final HttpURLConnection conn = (HttpURLConnection) (options.getProxy() == null ? url.openConnection()
                    : url.openConnection(options.getProxy()));
            conn.setConnectTimeout(options.getConnectTimeoutSeconds() * 1000);
            conn.setReadTimeout(options.getReadTimeoutSeconds() * 1000);
            conn.setUseCaches(false);
            conn.setDoInput(true);
            for (Map.Entry<String, String> entry : requestHeaderMap.entrySet())
                conn.addRequestProperty(entry.getKey(), entry.getValue());

            final InputStream inputStream = decryptInputStream(conn.getInputStream());
            return new BufferedInputStream(inputStream) {
                @Override
                public void close() throws IOException {
                    super.close();
                    conn.disconnect();
                }
            };
        }

        /**
         * .ts的下载
         */
        @Override
        public DownloadRunnable download(M3u8DownloadExecutor executorService, String storePath) {
            DownloadRunnable runnable = new DownloadRunnable(mediaName, fileName, nextFileName) {
                @Override
                public File getFile() {
                    return new File(storePath, fileName);
                }

                @Override
                public void run() {
                    final File file = getFile();
                    if (!options.isOverride() && file.exists()) {
                        return;
                    }

                    synchronized (this) {
                        if (!options.isOverride() && file.exists()) {
                            this.notifyAll();
                            return;
                        }
                        try {
                            final File tmpFile = new File(storePath, fileName + SUF_TMP);
                            final byte[] buffer = BUFFER.get();
                            int retry = 0;
                            while (retry++ < options.getRetryCount()) {
                                try (InputStream inputStream = getInputStream();
                                        FileOutputStream fos = new FileOutputStream(tmpFile)) {
                                    int len;
                                    while ((len = inputStream.read(buffer)) > 0) {
                                        fos.write(buffer, 0, len);
                                    }
                                    break;
                                } catch (IOException e) {
                                    if (retry < options.getRetryCount()) {
                                        logger.info("下载 {}/{} 失败，开始第{}次重试", mediaName, fileName, retry, e);
                                        tmpFile.delete();
                                        try {
                                            Thread.sleep(options.getRetryIntervalSeconds() * 1000);
                                            continue;
                                        } catch (InterruptedException e1) {
                                            logger.error("下载 {}/{} 中断", mediaName, fileName);
                                            return;
                                        }
                                    }
                                }
                            }
                            if (ctx.blockList.isBlockFile(tmpFile)) {
                                try (InputStream inputStream = M3u8Media.class.getResourceAsStream("/blank.ts");
                                        FileOutputStream fos = new FileOutputStream(tmpFile)) {
                                    int len;
                                    while ((len = inputStream.read(buffer)) > 0) {
                                        fos.write(buffer, 0, len);
                                    }
                                } catch (IOException e) {
                                    logger.error("屏蔽 {}/{} 失败", mediaName, fileName);
                                }
                            }
                            if (retry == options.getRetryCount()) {
                                logger.error("下载 {}/{} 失败，已重试{}次", mediaName, fileName, retry);
                                return;
                            }
                            if (options.isOverride() && file.exists()) {
                                file.delete();
                            }
                            tmpFile.renameTo(file);
                            logger.info("下载 {}/{} 成功", mediaName, fileName);
                        } finally {
                            this.notifyAll();
                        }
                    }
                }
            };
            executorService.execute(runnable);
            return runnable;
        }
    }

    /**
     * index.m3u8的下载
     */
    @Override
    public DownloadRunnable download(final M3u8DownloadExecutor executorService, String storePath) {
        DownloadRunnable runnable = new DownloadRunnable(mediaName, NAME_INDEX_M3U8, null) {
            @Override
            public File getFile() {
                return new File(storePath, NAME_INDEX_M3U8);
            }

            @Override
            public void run() {
                File storeFile = new File(storePath);
                if (!storeFile.exists()) {
                    storeFile.mkdirs();
                }
                synchronized (this) {
                    File file = getFile();
                    BlockList blockList = new BlockList(file);
                    DownloadContext ctx = new DownloadContext(blockList);
                    try {
                        if (!options.isOverride() && file.exists()) {
                            // 这种情况触发segment下载即可,无需写入m3u8文件
                            MediaPlaylist mediaPlaylist = getMediaPlaylist(file);
                            if (mediaPlaylist != null) {
                                List<MediaSegment> mediaSegments = mediaPlaylist.mediaSegments();
                                List<M3u8Segment> m3u8Segments = createM3u8Segments(mediaSegments, ctx);
                                startDownloadSegments(executorService, storePath, m3u8Segments);
                                return;
                            }
                        }

                        final MediaPlaylist mediaPlaylist = getMediaPlaylist(originUrl);
                        writePlaylist(new File(storePath, NAME_INDEX_M3U8_ORIGIN), mediaPlaylist);

                        List<M3u8Segment> m3u8Segments = createM3u8Segments(mediaPlaylist.mediaSegments(), ctx);

                        MediaPlaylist newPlaylist = createLocalPlaylist(mediaPlaylist);
                        writePlaylist(file, newPlaylist);

                        if (blockList.hasBlockUrl()) {
                            File noAdsFile = new File(file.getParent(), NAME_INDEX_M3U8_NOADS);
                            MediaPlaylist newNoAdsPlaylist = createNoAdsPlaylist(newPlaylist, blockList);
                            writePlaylist(noAdsFile, newNoAdsPlaylist);
                        }

                        startDownloadSegments(executorService, storePath, m3u8Segments);
                    } catch (Exception e) {
                        logger.error("下载{}失败", mediaName, e);
                    } finally {
                        this.notifyAll();
                    }
                }
            }

            /**
             * 解析媒体播放列表
             * 
             * @param url 主播放列表或媒体播放列表URL
             * @return 返回最优的媒体播放列表
             */
            private MediaPlaylist getMediaPlaylist(String url) {

                MasterPlaylist playlist;
                try {
                    while (true) {
                        HttpURLConnection connection = (HttpURLConnection) new URI(url).toURL().openConnection();
                        connection.setInstanceFollowRedirects(false);

                        int responseCode = connection.getResponseCode();
                        if (responseCode == HttpURLConnection.HTTP_MOVED_PERM
                                || responseCode == HttpURLConnection.HTTP_MOVED_TEMP
                                || responseCode == HttpURLConnection.HTTP_SEE_OTHER) {
                            url = connection.getHeaderField("Location");
                            continue;
                        } else if (responseCode == HttpURLConnection.HTTP_OK) {
                            String contentType = connection.getHeaderField("Content-Type");
                            if (contentType != null) {
                                contentType = contentType.toLowerCase();
                                if (contentType.startsWith("application/json")) {
                                    JsonNode tree = OBJ_MAPPER.readTree(getContent(url));
                                    if ("1".equals(tree.get("code").asText("0"))) {
                                        JsonNode dataObj = tree.get("data");
                                        String url3 = dataObj.get("url").asText(null);
                                        if (url3 != null) {
                                            url = url3;
                                        }
                                        JsonNode headerNode = dataObj.get("header");
                                        HashMap<String, String> headerMap = new HashMap<>();
                                        Iterator<Entry<String, JsonNode>> fieldIterator = headerNode.fields();
                                        while (fieldIterator.hasNext()) {
                                            Entry<String, JsonNode> entry = fieldIterator.next();
                                            headerMap.put(entry.getKey(), entry.getValue().asText(""));
                                        }
                                        requestHeaderMap = headerMap;
                                    }
                                    continue;
                                } else if (!contentType.startsWith("application/vnd.apple.mpegurl")) {
                                    logger.warn("下载{}的{}不具备application/vnd.apple.mpegurl类型头", mediaName, url);
                                }
                            }
                        }

                        playlist = masterPlaylistParser.readPlaylist(connection.getInputStream());
                        break;
                    }
                } catch (PlaylistParserException e) {
                    return getMediaPlaylist0(url);
                } catch (IOException | URISyntaxException e) {
                    throw new M3u8RuntimeException(e.getMessage() + ": " + url, e);
                }

                List<Variant> variants = playlist.variants();
                Variant variant = null;
                if (variants.size() == 1) {
                    variant = variants.get(0);
                } else if (variants.size() > 1) {
                    // TODO: 根据算法选择最佳
                    variant = variants.get(0);
                } else {
                    return getMediaPlaylist0(url);
                }
                String variantUri = variant.uri();
                return getMediaPlaylist0(mergeUrl(variantUri, getRelativeUrl(url)));
            }

            /**
             * 解析媒体播放列表
             * 
             * @param url 主播放列表或媒体播放列表URL
             * @return 返回最优的媒体播放列表
             */
            private MediaPlaylist getMediaPlaylist0(String url) {
                MediaPlaylist playlist;
                try {
                    playlist = mediaPlaylistParser.readPlaylist(new URI(url).toURL().openStream());
                } catch (IOException | URISyntaxException e) {
                    throw new M3u8RuntimeException(e.getMessage() + ": " + url, e);
                }

                String relativeUrl = getRelativeUrl(url);
                MediaSegment.Builder segmentBuilder = MediaSegment.builder();
                SegmentKey.Builder keyBuilder = SegmentKey.builder();
                List<MediaSegment> mediaSegment = new ArrayList<>(playlist.mediaSegments().size());
                for (MediaSegment segment : playlist.mediaSegments()) {
                    segmentBuilder.from(segment);
                    segmentBuilder.uri(mergeUrl(segment.uri(), relativeUrl));
                    if (segment.segmentKey().isPresent()) {
                        SegmentKey segmentKey = segment.segmentKey().get();
                        if (segmentKey.method() != KeyMethod.NONE) {
                            SegmentKey segmentKey2 = keyBuilder.from(segmentKey)
                                    .uri(mergeUrl(segmentKey.uri().get(), relativeUrl))
                                    .build();
                            segmentBuilder.segmentKey(segmentKey2);
                        }
                    }
                    mediaSegment.add(segmentBuilder.build());
                }
                return MediaPlaylist.builder().from(playlist).mediaSegments(mediaSegment).build();
            }

            private MediaPlaylist getMediaPlaylist(File file) {
                if (NAME_INDEX_M3U8.equals(file.getName())) {
                    file = new File(file.getParentFile(), NAME_INDEX_M3U8_ORIGIN);
                }
                if (!file.exists()) {
                    return null;
                }
                MediaPlaylist playlist;
                try {
                    playlist = mediaPlaylistParser.readPlaylist(new FileInputStream(file));
                } catch (IOException e) {
                    throw new M3u8RuntimeException(e.getMessage() + ": " + file, e);
                }
                return playlist;
            }

            private String getContent(String url) {
                try {
                    URL urlObj = new URL(url);
                    HttpURLConnection connection = (HttpURLConnection) urlObj.openConnection();
                    connection.setRequestMethod("GET");

                    BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
                    String inputLine;
                    StringBuffer response = new StringBuffer();

                    while ((inputLine = in.readLine()) != null) {
                        response.append(inputLine).append('\n');
                    }
                    in.close();
                    return response.toString();
                } catch (Exception e) {
                    logger.error("parse url faield: {}", url, e);
                    return null;
                }
            }

            private void startDownloadSegments(final M3u8DownloadExecutor executorService, String storePath,
                    List<M3u8Segment> m3u8Segments) {
                DownloadRunnable firstRunnable = null;
                for (M3u8Segment m3u8Segment : m3u8Segments) {
                    DownloadRunnable download = m3u8Segment.download(executorService, storePath);
                    if (download != null && firstRunnable == null) {
                        firstRunnable = download;
                    }
                }
                if (firstRunnable != null) {
                    executorService.recordHead(firstRunnable.getIndexName());
                }
            }

            private List<M3u8Segment> createM3u8Segments(List<MediaSegment> mediaSegments, DownloadContext ctx) {
                List<M3u8Segment> m3u8Segments = new ArrayList<>(mediaSegments.size());
                for (int i = 0, size = mediaSegments.size(); i < size; i++) {
                    final MediaSegment mediaSegment = mediaSegments.get(i);
                    final String fileName = (i + 1) + SUF_TS;
                    final String nextName = i + 1 < size ? (i + 2) + SUF_TS : null;
                    final M3u8Segment m3u8Segment = new M3u8Segment(ctx, mediaSegment, fileName, nextName);
                    m3u8Segments.add(m3u8Segment);
                }
                return m3u8Segments;
            }

            private MediaPlaylist createLocalPlaylist(final MediaPlaylist mediaPlaylist) {
                List<MediaSegment> mediaSegments = mediaPlaylist.mediaSegments();
                MediaSegment.Builder segmentbuilder = MediaSegment.builder();
                List<MediaSegment> newMediaSegments = new ArrayList<>(mediaSegments.size());
                for (int i = 0, size = mediaSegments.size(); i < size; i++) {
                    final MediaSegment mediaSegment = mediaSegments.get(i);
                    final String fileName = (i + 1) + SUF_TS;
                    MediaSegment newMediaSegment = segmentbuilder.from(mediaSegment).uri(fileName)
                            .segmentKey(Optional.empty()).discontinuity(false).title(mediaSegment.uri()).build();
                    newMediaSegments.add(newMediaSegment);
                }
                MediaPlaylist.Builder playListbuilder = MediaPlaylist.builder();
                MediaPlaylist newPlaylist = playListbuilder.from(mediaPlaylist).mediaSegments(newMediaSegments)
                        .build();
                return newPlaylist;
            }

            private MediaPlaylist createNoAdsPlaylist(final MediaPlaylist mediaPlaylist, BlockList blockList) {
                List<MediaSegment> mediaSegments = mediaPlaylist.mediaSegments();
                List<MediaSegment> newMediaSegments = new ArrayList<>(mediaSegments.size());

                for (int i = 0, size = mediaSegments.size(); i < size; i++) {
                    final MediaSegment mediaSegment = mediaSegments.get(i);
                    Optional<String> title = mediaSegment.title();
                    if (title.isPresent()) {
                        if (blockList.isBlockUrl(title.get())) {
                            continue;
                        }
                    }
                    newMediaSegments.add(mediaSegment);
                }
                MediaPlaylist.Builder playListbuilder = MediaPlaylist.builder();
                MediaPlaylist newPlaylist = playListbuilder.from(mediaPlaylist).mediaSegments(newMediaSegments)
                        .build();
                return newPlaylist;
            }
        };

        executorService.execute(runnable);
        return runnable;
    }

    /**
     * 模拟http请求获取Key内容
     *
     * @param url0 http链接
     * @return 返回key字节数组
     * @throws MalformedURLException
     */
    private byte[] getKeyUrlContent(String url0) throws MalformedURLException {
        int count = 1;
        HttpURLConnection httpURLConnection = null;
        URL url = new URL(url0);
        while (count <= options.getRetryCount()) {
            try {
                if (options.getProxy() == null) {
                    httpURLConnection = (HttpURLConnection) url.openConnection();
                } else {
                    httpURLConnection = (HttpURLConnection) url.openConnection(options.getProxy());
                }
                httpURLConnection.setConnectTimeout(options.getConnectTimeoutSeconds() * 1000);
                httpURLConnection.setReadTimeout(options.getReadTimeoutSeconds() * 1000);
                httpURLConnection.setUseCaches(false);
                httpURLConnection.setDoInput(true);
                for (Map.Entry<String, String> entry : requestHeaderMap.entrySet())
                    httpURLConnection.addRequestProperty(entry.getKey(), entry.getValue());
                try (InputStream inputStream = httpURLConnection.getInputStream()) {
                    byte[] bytes = new byte[128];
                    int len = inputStream.read(bytes);

                    if (len == bytes.length) {
                        return bytes;
                    } else {
                        return Arrays.copyOf(bytes, len);
                    }
                }
            } catch (Exception e) {
                logger.info("第{}获取链接重试:{}", count, url0);
                count++;
            } finally {
                if (httpURLConnection != null) {
                    httpURLConnection.disconnect();
                }
            }
        }
        throw new M3u8RuntimeException("连接超时！");
    }

    /**
     * 将播放列表数据写入到磁盘
     * 
     * @param file     文件
     * @param playlist 数据
     */
    private static void writePlaylist(File file, MediaPlaylist playlist) {
        byte[] writePlaylistAsBytes = mediaPlaylistParser.writePlaylistAsBytes(playlist);
        try (FileOutputStream fos = new FileOutputStream(file)) {
            fos.write(writePlaylistAsBytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取基础路径
     * 
     * @param url 路径
     * @return 返回基础路径(保证最后一定有/)
     */
    private static String getRelativeUrl(String url) {
        int lastIndexOf = url.lastIndexOf("/");
        if (lastIndexOf >= 0) {
            return url.substring(0, lastIndexOf) + "/";
        } else {
            return url + "/";
        }
    }

    /**
     * 是否是一个完整路径
     * 
     * @param url 路径
     * @return 是否为完整路径
     */
    private static boolean isFullUrl(String url) {
        return url.matches("https?://.*");
    }

    /**
     * 拼接为完整URL
     * 
     * @param uri     路径
     * @param baseUri 基础路径
     * @return 返回完整路径
     */
    private static String mergeUrl(String uri, String baseUri) {
        if (isFullUrl(uri)) {
            return uri;
        } else {
            if (uri.startsWith("/")) {
                return baseUri.replaceAll("^(https?://[^/]+).*$", "$1") + uri;
            } else {
                return baseUri + uri;
            }
        }
    }

}
